import { expect } from "chai";
import hre, { deployments, ethers } from "hardhat";
import { AddressZero } from "@ethersproject/constants";
import { defaultAbiCoder } from "@ethersproject/abi";
import { getSafeWithOwners, deployContract, getCompatFallbackHandler } from "../utils/setup";
import { buildSignatureBytes, executeContractCallWithSigners, signHash } from "../../src/utils/execution";

describe("Safe", () => {
    const setupTests = deployments.createFixture(async ({ deployments }) => {
        await deployments.fixture();
        const handler = await getCompatFallbackHandler();
        const handlerAddress = await handler.getAddress();
        const signers = await ethers.getSigners();
        const [user1, user2] = signers;
        const ownerSafe = await getSafeWithOwners([user1.address, user2.address], 2, handlerAddress);
        const ownerSafeAddress = await ownerSafe.getAddress();
        const messageHandler = await getCompatFallbackHandler(ownerSafeAddress);
        return {
            safe: await getSafeWithOwners([ownerSafeAddress, user1.address], 1),
            ownerSafe,
            messageHandler,
            signers,
        };
    });

    describe("0xExploit", () => {
        /*
         * In case of 0x it was possible to use EIP-1271 (contract signatures) to generate a valid signature for EOA accounts.
         * See https://samczsun.com/the-0x-vulnerability-explained/
         */
        it("should not be able to use EIP-1271 (contract signatures) for EOA", async () => {
            const {
                safe,
                ownerSafe,
                messageHandler,
                signers: [user1, user2],
            } = await setupTests();
            const ownerSafeAddress = await ownerSafe.getAddress();
            // Safe should be empty again
            await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther("1") });
            await expect(await hre.ethers.provider.getBalance(await safe.getAddress())).to.eq(ethers.parseEther("1"));

            const operation = 0;
            const to = user1.address;
            const value = ethers.parseEther("1");
            const data = "0x";
            const nonce = await safe.nonce();

            // Use off-chain Safe signature
            const transactionHash = await safe.getTransactionHash(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, nonce);
            const messageHash = await messageHandler.getMessageHash(transactionHash);
            const ownerSigs = await buildSignatureBytes([await signHash(user1, messageHash), await signHash(user2, messageHash)]);
            const encodedOwnerSigns = defaultAbiCoder.encode(["bytes"], [ownerSigs]).slice(66);

            // Use EOA owner
            let sigs =
                "0x" +
                "000000000000000000000000" +
                user2.address.slice(2) +
                "0000000000000000000000000000000000000000000000000000000000000041" +
                "00" + // r, s, v
                encodedOwnerSigns;

            // Transaction should fail (invalid signatures should revert the Ethereum transaction)
            await expect(
                safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs),
                "Transaction should fail if invalid signature is provided",
            ).to.be.reverted;
            await expect(await hre.ethers.provider.getBalance(await safe.getAddress())).to.eq(ethers.parseEther("1"));

            // Use Safe owner
            sigs =
                "0x" +
                "000000000000000000000000" +
                ownerSafeAddress.slice(2) +
                "0000000000000000000000000000000000000000000000000000000000000041" +
                "00" + // r, s, v
                encodedOwnerSigns;

            await safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs);

            // Safe should be empty again
            await expect(await hre.ethers.provider.getBalance(await safe.getAddress())).to.eq(ethers.parseEther("0"));
        });

        it("should revert if EIP-1271 check changes state", async () => {
            const {
                safe,
                ownerSafe,
                messageHandler,
                signers: [user1, user2],
            } = await setupTests();
            const ownerSafeAddress = await ownerSafe.getAddress();
            // Test Validator
            const source = `
            contract Test {
                bool public changeState;
                uint256 public nonce;
                function isValidSignature(bytes32 _data, bytes memory _signature) public returns (bytes4) {
                    if (changeState) {
                        nonce = nonce + 1;
                    }
                    return 0x1626ba7e;
                }
    
                function shouldChangeState(bool value) public {
                    changeState = value;
                }
            }`;
            const testValidator = await deployContract(user1, source);
            const testValidatorAddress = await testValidator.getAddress();
            await testValidator.shouldChangeState(true);

            await executeContractCallWithSigners(safe, safe, "addOwnerWithThreshold", [testValidatorAddress, 1], [user1]);
            await expect(await safe.getOwners()).to.be.deep.eq([testValidatorAddress, ownerSafeAddress, user1.address]);

            // Deposit 1 ETH + some spare money for execution
            await user1.sendTransaction({ to: await safe.getAddress(), value: ethers.parseEther("1") });
            await expect(await hre.ethers.provider.getBalance(await safe.getAddress())).to.eq(ethers.parseEther("1"));

            const operation = 0;
            const to = user1.address;
            const value = ethers.parseEther("1");
            const data = "0x";
            const nonce = await safe.nonce();

            // Use off-chain Safe signature
            const transactionHash = await safe.getTransactionHash(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, nonce);
            const messageHash = await messageHandler.getMessageHash(transactionHash);
            const ownerSigs = await buildSignatureBytes([await signHash(user1, messageHash), await signHash(user2, messageHash)]);
            const encodedOwnerSigns = defaultAbiCoder.encode(["bytes"], [ownerSigs]).slice(66);

            // Use Safe owner
            const sigs =
                "0x" +
                "000000000000000000000000" +
                testValidatorAddress.slice(2) +
                "0000000000000000000000000000000000000000000000000000000000000041" +
                "00" + // r, s, v
                encodedOwnerSigns;

            // Transaction should fail (state changing signature check should revert)
            await expect(
                safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs),
                "Transaction should fail if invalid signature is provided",
            ).to.be.reverted;
            await expect(await hre.ethers.provider.getBalance(await safe.getAddress())).to.eq(ethers.parseEther("1"));

            await testValidator.shouldChangeState(false);
            await safe.execTransaction(to, value, data, operation, 0, 0, 0, AddressZero, AddressZero, sigs);

            // Safe should be empty again
            await expect(await hre.ethers.provider.getBalance(await safe.getAddress())).to.eq(ethers.parseEther("0"));
        });
    });
});
